﻿using System;
using System.Net;
using System.Collections.Generic;
using System.Text;
using System.Text.RegularExpressions;
using System.Diagnostics;
using System.Xml.Linq;

﻿namespace kevin.zhang.canada.at.gmail.com
{
    static public class Delta
    {
        public class ChangeSet
        {
            public Dictionary<int, ChangeItem> ChangeItemCollection;
            public override string ToString()
            {
                string ret = string.Empty;
                foreach (var kvp in ChangeItemCollection)
                {
                    int lineNumber = kvp.Key;
                    ChangeItem item = kvp.Value;

                    ret += string.Format("\r\nLine# or Word#:{0}-{1}\r\n", lineNumber.ToString(), item.changeType);
                    if (item.changeType == ChangeItem.ChangeType.Delete || item.changeType == ChangeItem.ChangeType.Replace
                        || item.changeType == ChangeItem.ChangeType.Update)
                    {
                        ret += "\r\n\t<<<<<\r\n\t" + "Original Ignored";
                    }

                    if (item.InsertStrList != null)
                    {
                        foreach (var insertStr in item.InsertStrList)
                        {
                            ret += "\r\n\t>>>>>\r\n\t" + insertStr + "\r\n";
                        }
                    }
                    if (item.changeType == ChangeItem.ChangeType.Update && item.deltaWordInOneLine != null)
                    {
                        ret += "\r\n=======The following changes are words in one line=============\r\n";
                        ChangeSet changeSetInLine = new ChangeSet { ChangeItemCollection = item.deltaWordInOneLine };
                        ret += changeSetInLine.ToString();
                        ret += "\r\n=======The above changes are words in one line=============\r\n";
                    }
                }
                return ret;

            }
        }

        public class ChangeItem
        {
            public enum ChangeType
            {
                NotDefined = 0,
                Delete = 1,
                Insert = 2,
                Replace = 3,
                Update = 4
            };
            public ChangeType changeType = ChangeType.NotDefined;
            public string[] InsertStrList = null;
            public Dictionary<int, ChangeItem> deltaWordInOneLine = null;
            public ChangeItem()
            {
            }
        };

        public static ChangeSet GetChangeSet(XElement source, XElement target)
        {
            string[] sourceStrArray = source.ToString(SaveOptions.None).Split('\n');
            string[] targetStrArray = target.ToString(SaveOptions.None).Split('\n');
            return GetChangeSet(sourceStrArray, targetStrArray);
        }

        public static ChangeSet GetChangeSet(string source, string target)
        {
            return GetChangeSet(source.Split('\n'), target.Split('\n'));
        }

        public static ChangeSet GetChangeSet(string[] source, string[] target)
        {
            return new ChangeSet { ChangeItemCollection = GetDelta(source, target) };
        }

        public static string[] ApplyDelta(string[] original, Dictionary<int, ChangeItem> changeItemCollection)
        {
            return ApplyDelta(original, changeItemCollection, false);
        }

        public static XElement ApplyChangeSet(XElement source, ChangeSet changeSet)
        {
            string[] sourceStrArray = source.ToString(SaveOptions.None).Split('\n');
            string[] destStrArry = ApplyChangeSet(sourceStrArray, changeSet);
            string xmlString = string.Empty;
            foreach (var str in destStrArry)
            {
                xmlString += str;
            }
            return XElement.Parse(xmlString);
        }

        public static string[] ApplyChangeSet(string[] source, ChangeSet changeSet)
        {
            return ApplyDelta(source, changeSet.ChangeItemCollection, false);
        }

        public static string ApplyChangeSet(string source, ChangeSet changeSet)
        {
            string destString = string.Empty;
            string[] sourceStrArray = source.Split('\n');
            string[] destStrArray = ApplyChangeSet(sourceStrArray, changeSet);

            foreach (var str in destStrArray)
            {
                destString += str + "\n";
            }
            if (string.IsNullOrEmpty(destString) == false)
            {
                destString = destString.Substring(0, destString.Length - 1);
            }
            return destString;//remove the last '\n'.
        }

        internal static string[] ApplyDelta(string[] original, Dictionary<int, ChangeItem> changeItemCollection, bool isApplyInOneSentence)
        {
            List<string> destStringList = new List<string>();

            for (int i = 0; i < original.Length; i++)
            {
                if (changeItemCollection.ContainsKey(i) == false)
                {
                    //this line has no change, just copy it over
                    destStringList.Add(original[i]);
                    continue;
                }
                //this line has some kind of change
                ChangeItem item = changeItemCollection[i];

                Debug.Assert(item.changeType != ChangeItem.ChangeType.NotDefined);

                switch (item.changeType)
                {
                    case ChangeItem.ChangeType.Delete:
                        //just do not copy the original line
                        Debug.Assert(item.InsertStrList == null, "If ChangeType is delete, we must NOT have InsertStrList to insert");
                        break;
                    case ChangeItem.ChangeType.Insert:
                        Debug.Assert(item.InsertStrList != null, "If ChangeType is Insert, we must have a InsertStrList to insert");
                        foreach (string insertStr in item.InsertStrList)
                        {
                            destStringList.Add(insertStr);
                        }
                        //Since it is 'insert', we still need the original line
                        destStringList.Add(original[i]);
                        //if (lineNumber != -1)
                        //{
                        //    destStringList.Add(original[lineNumber]);
                        //}
                        break;
                    case ChangeItem.ChangeType.Replace:
                        Debug.Assert(item.InsertStrList != null, "If ChangeType is Replace, we must have a InsertStrList to insert");
                        foreach (string insertStr in item.InsertStrList)
                        {
                            destStringList.Add(insertStr);
                        }
                        //since it is not 'insert', it is 'replace', the original line is dropped
                        break;
                    case ChangeItem.ChangeType.Update:
                        {
                            if (item.deltaWordInOneLine != null)
                            {
                                Debug.Assert(isApplyInOneSentence == false);
                                string[] lineChangeDest = ApplyDelta(original[i].Split(' '), item.deltaWordInOneLine, true);
                                string strNeedAdd = string.Empty;
                                foreach (var str in lineChangeDest)
                                {
                                    strNeedAdd += str + ' ';//add the 'space' for each line. The space was removed when we make diff
                                }
                                if (string.IsNullOrEmpty(strNeedAdd) == false)
                                {
                                    strNeedAdd = strNeedAdd.Substring(0, strNeedAdd.Length - 1);//substring is because remove the last space which is added in the loop
                                }
                                destStringList.Add(strNeedAdd);
                            }
                            else
                            {
                                Debug.Assert(isApplyInOneSentence == true);
                                if (item.InsertStrList != null)
                                {
                                    foreach (string str in item.InsertStrList)
                                    {
                                        destStringList.Add(str);
                                    }
                                }
                            }
                        }
                        break;
                }
            }

            //now handle the inserting lines at the end of the source
            if (changeItemCollection.ContainsKey(original.Length))
            {
                foreach (string strAddedAtEnd in changeItemCollection[original.Length].InsertStrList)
                {
                    destStringList.Add(strAddedAtEnd);
                }
            }
            return destStringList.ToArray();
        }

        internal static Dictionary<int, ChangeItem> GetDelta(string[] strA, string[] strB)
        {
            return GetDelta(strA, strB, false);
        }

        internal static Dictionary<int, ChangeItem> GetDelta(string[] strA, string[] strB, bool isDiffInOneLine)
        {
            Diff.Item[] f = Diff.DiffText(strA, strB, false, false, false);
            Dictionary<int, ChangeItem> changeItemCollection = new Dictionary<int, ChangeItem>();

            string ret = string.Empty;
            for (int n = 0; n < f.Length; n++)
            {
                Diff.Item item = f[n];
                if (item.deletedA > 0)
                {
                    for (int d = 0; d < item.deletedA; d++)
                    {
                        if (changeItemCollection.ContainsKey(item.StartA + d) == false)
                        {
                            changeItemCollection.Add(item.StartA + d, new ChangeItem());
                        }
                        changeItemCollection[item.StartA + d].changeType = ChangeItem.ChangeType.Delete;
                    }
                }
                if (item.insertedB > 0)
                {
                    if (changeItemCollection.ContainsKey(item.StartA) && changeItemCollection[item.StartA].changeType == ChangeItem.ChangeType.Delete)
                    {
                        if (item.deletedA == item.insertedB)
                        {
                            for (int u = 0; u < item.deletedA; u++)
                            {
                                if (changeItemCollection.ContainsKey(item.StartA + u) == false)
                                {
                                    changeItemCollection.Add(item.StartA + u, new ChangeItem());
                                }
                                changeItemCollection[item.StartA + u].changeType = ChangeItem.ChangeType.Update;
                                if (isDiffInOneLine == true)
                                {
                                    changeItemCollection[item.StartA + u].InsertStrList = new string[1];
                                    changeItemCollection[item.StartA + u].InsertStrList[0] = strB[item.StartB + u];
                                }
                                //now deal with the Update type. we need to drill down into this string, seperate it into pieces (by space) and delta diff again
                                else
                                {
                                    //Debug.Assert(strResult[item.StartA].InsertStrList.Length == 1, "I assume for every type=Update, there should be only one line. If not, we need to consider the logic again");
                                    string original = strA[item.StartA + u];
                                    string changeTo = strB[item.StartB + u];// strResult[item.StartA].InsertStrList[0];
                                    //use space to seperate
                                    string[] originalStrList = original.Split(' ');
                                    string[] changeToStrList = changeTo.Split(' ');
                                    changeItemCollection[item.StartA + u].deltaWordInOneLine = GetDelta(originalStrList, changeToStrList, true);
                                }
                            }
                        }
                        else
                        {
                            if (changeItemCollection.ContainsKey(item.StartA) == false)
                            {
                                changeItemCollection.Add(item.StartA, new ChangeItem());
                            }
                            changeItemCollection[item.StartA].changeType = ChangeItem.ChangeType.Replace;
                        }
                    }
                    else
                    {
                        if (changeItemCollection.ContainsKey(item.StartA) == false)
                        {
                            changeItemCollection.Add(item.StartA, new ChangeItem());
                        }
                        changeItemCollection[item.StartA].changeType = ChangeItem.ChangeType.Insert;
                    }
                    if (changeItemCollection.ContainsKey(item.StartA) == false)
                    {
                        changeItemCollection.Add(item.StartA, new ChangeItem());
                    }
                    if (changeItemCollection[item.StartA].InsertStrList == null
                        &&
                        (changeItemCollection[item.StartA].changeType == ChangeItem.ChangeType.Insert || changeItemCollection[item.StartA].changeType == ChangeItem.ChangeType.Replace))
                    {
                        changeItemCollection[item.StartA].InsertStrList = new string[item.insertedB];
                        for (int i = 0; i < item.insertedB; i++)
                        {
                            changeItemCollection[item.StartA].InsertStrList[i] = strB[item.StartB + i];
                        }
                    }
                }
            }

            return changeItemCollection;
        }
    }
} // namespace


